msg_tool\scripts\cat_system\archive/
int.rs1use super::twister::MersenneTwister;
3use crate::ext::io::*;
4use crate::scripts::base::*;
5use crate::types::*;
6use crate::utils::blowfish::Blowfish;
7use crate::utils::crc32::CRC32NORMAL_TABLE;
8use crate::utils::encoding::{decode_to_string, encode_string};
9use anyhow::Result;
10use overf::wrapping;
11use std::io::{Read, Seek, SeekFrom};
12use std::sync::{Arc, Mutex};
13
14pub use super::int_password::get_password_from_exe;
15
16#[derive(Debug)]
17pub struct CSIntArcBuilder {}
19
20impl CSIntArcBuilder {
21 pub fn new() -> Self {
23 CSIntArcBuilder {}
24 }
25}
26
27impl ScriptBuilder for CSIntArcBuilder {
28 fn default_encoding(&self) -> Encoding {
29 Encoding::Cp932
30 }
31
32 fn default_archive_encoding(&self) -> Option<Encoding> {
33 Some(Encoding::Cp932)
34 }
35
36 fn build_script(
37 &self,
38 data: Vec<u8>,
39 filename: &str,
40 _encoding: Encoding,
41 archive_encoding: Encoding,
42 config: &ExtraConfig,
43 _archive: Option<&Box<dyn Script>>,
44 ) -> Result<Box<dyn Script>> {
45 Ok(Box::new(CSIntArc::new(
46 MemReader::new(data),
47 archive_encoding,
48 config,
49 filename,
50 )?))
51 }
52
53 fn build_script_from_file(
54 &self,
55 filename: &str,
56 _encoding: Encoding,
57 archive_encoding: Encoding,
58 config: &ExtraConfig,
59 _archive: Option<&Box<dyn Script>>,
60 ) -> Result<Box<dyn Script>> {
61 if filename == "-" {
62 let data = crate::utils::files::read_file(filename)?;
63 Ok(Box::new(CSIntArc::new(
64 MemReader::new(data),
65 archive_encoding,
66 config,
67 filename,
68 )?))
69 } else {
70 let f = std::fs::File::open(filename)?;
71 let reader = std::io::BufReader::new(f);
72 Ok(Box::new(CSIntArc::new(
73 reader,
74 archive_encoding,
75 config,
76 filename,
77 )?))
78 }
79 }
80
81 fn build_script_from_reader(
82 &self,
83 reader: Box<dyn ReadSeek>,
84 filename: &str,
85 _encoding: Encoding,
86 archive_encoding: Encoding,
87 config: &ExtraConfig,
88 _archive: Option<&Box<dyn Script>>,
89 ) -> Result<Box<dyn Script>> {
90 Ok(Box::new(CSIntArc::new(
91 reader,
92 archive_encoding,
93 config,
94 filename,
95 )?))
96 }
97
98 fn extensions(&self) -> &'static [&'static str] {
99 &["int"]
100 }
101
102 fn script_type(&self) -> &'static ScriptType {
103 &ScriptType::CatSystemInt
104 }
105
106 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
107 if buf_len >= 4 && buf.starts_with(b"KIF\0") {
108 return Some(10);
109 }
110 None
111 }
112
113 fn is_archive(&self) -> bool {
114 true
115 }
116}
117
118fn detect_script_type(buf: &[u8], buf_len: usize, _filename: &str) -> Option<&'static ScriptType> {
119 #[cfg(feature = "cat-system-img")]
120 if buf_len >= 4 && buf.starts_with(b"HG-3") {
121 return Some(&ScriptType::CatSystemHg3);
122 }
123 if buf_len >= 8 && buf.starts_with(b"CatScene") {
124 return Some(&ScriptType::CatSystem);
125 }
126 if buf_len >= 4 && buf.starts_with(b"CSTL") {
127 return Some(&ScriptType::CatSystemCstl);
128 }
129 None
130}
131
132#[derive(Clone, Debug)]
133struct CSIntFileHeader {
134 name: String,
135 offset: u32,
136 size: u32,
137}
138
139struct Entry<T: Read + Seek> {
140 header: CSIntFileHeader,
141 reader: Arc<Mutex<T>>,
142 pos: usize,
143 script_type: Option<ScriptType>,
144}
145
146impl<T: Read + Seek> ArchiveContent for Entry<T> {
147 fn name(&self) -> &str {
148 &self.header.name
149 }
150
151 fn script_type(&self) -> Option<&ScriptType> {
152 self.script_type.as_ref()
153 }
154}
155
156impl<T: Read + Seek> Read for Entry<T> {
157 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
158 let mut reader = self.reader.lock().map_err(|e| {
159 std::io::Error::new(
160 std::io::ErrorKind::Other,
161 format!("Failed to lock mutex: {}", e),
162 )
163 })?;
164 reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
165 let bytes_read = buf.len().min(self.header.size as usize - self.pos);
166 if bytes_read == 0 {
167 return Ok(0);
168 }
169 let bytes_read = reader.read(&mut buf[..bytes_read])?;
170 self.pos += bytes_read;
171 Ok(bytes_read)
172 }
173}
174
175impl<T: Read + Seek> Seek for Entry<T> {
176 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
177 let new_pos = match pos {
178 SeekFrom::Start(offset) => offset as usize,
179 SeekFrom::End(offset) => {
180 if offset < 0 {
181 if (-offset) as usize > self.header.size as usize {
182 return Err(std::io::Error::new(
183 std::io::ErrorKind::InvalidInput,
184 "Seek from end exceeds file length",
185 ));
186 }
187 self.header.size as usize - (-offset) as usize
188 } else {
189 self.header.size as usize + offset as usize
190 }
191 }
192 SeekFrom::Current(offset) => {
193 if offset < 0 {
194 if (-offset) as usize > self.pos {
195 return Err(std::io::Error::new(
196 std::io::ErrorKind::InvalidInput,
197 "Seek from current exceeds current position",
198 ));
199 }
200 self.pos.saturating_sub((-offset) as usize)
201 } else {
202 self.pos + offset as usize
203 }
204 }
205 };
206 self.pos = new_pos;
207 Ok(self.pos as u64)
208 }
209
210 fn stream_position(&mut self) -> std::io::Result<u64> {
211 Ok(self.pos as u64)
212 }
213}
214
215struct MemEntry {
216 name: String,
217 data: MemReader,
218}
219
220impl Read for MemEntry {
221 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
222 self.data.read(buf)
223 }
224}
225
226impl ArchiveContent for MemEntry {
227 fn name(&self) -> &str {
228 &self.name
229 }
230
231 fn script_type(&self) -> Option<&ScriptType> {
232 detect_script_type(&self.data.data, self.data.data.len(), &self.name)
233 }
234
235 fn data(&mut self) -> Result<Vec<u8>> {
236 Ok(self.data.data.clone())
237 }
238
239 fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
240 Ok(Box::new(&mut self.data))
241 }
242}
243
244#[derive(Debug)]
245pub struct CSIntArc<T: Read + Seek + std::fmt::Debug> {
247 reader: Arc<Mutex<T>>,
248 encrypt: Option<Blowfish>,
249 entries: Vec<CSIntFileHeader>,
250}
251
252const NAME_SIZES: [usize; 2] = [0x20, 0x40];
253
254impl<T: Read + Seek + std::fmt::Debug> CSIntArc<T> {
255 pub fn new(
262 mut reader: T,
263 archive_encoding: Encoding,
264 config: &ExtraConfig,
265 _filename: &str,
266 ) -> Result<Self> {
267 let mut magic = [0u8; 4];
268 reader.read_exact(&mut magic)?;
269 if &magic != b"KIF\0" {
270 return Err(anyhow::anyhow!(
271 "Invalid magic number for CatSystem2 archive"
272 ));
273 }
274 let entry_count = reader.read_u32()?;
275 let mut keybuf = [0u8; 12];
276 reader.read_exact(&mut keybuf)?;
277 if &keybuf == b"__key__.dat\0" {
278 let key = match &config.cat_system_int_encrypt_password {
279 Some(password) => Self::get_key(password)?,
280 None => {
281 return Err(anyhow::anyhow!(
282 "CatSystem2 archive requires encryption password. Please use --cat-system-int-encrypt-password/--cat-system-int-exe option."
283 ));
284 }
285 };
286 eprintln!("Using CatSystem2 archive encryption key: {key:08X}");
287 let seed = reader.peek_u32_at(0x4C)?;
288 let mut twister = MersenneTwister::new(seed);
289 let blowfish_key = twister.rand().to_le_bytes();
290 let encrypt = match Blowfish::new(&blowfish_key) {
291 Ok(bf) => bf,
292 Err(e) => {
293 return Err(anyhow::anyhow!("Failed to create Blowfish cipher: {}", e));
294 }
295 };
296 let mut entries = Vec::with_capacity(entry_count as usize - 1);
297 let mut name_buf = [0u8; 0x40];
298 reader.seek(SeekFrom::Start(0x50))?;
299 for i in 1..entry_count {
300 reader.read_exact(&mut name_buf)?;
301 let offset = reader.read_u32()? + i;
302 let size = reader.read_u32()?;
303 let decryped = encrypt.decrypt([offset, size]);
304 twister.s_rand(key + i);
305 let name_key = twister.rand();
306 let name = Self::decrypt_name(&mut name_buf, name_key, archive_encoding)?;
307 let entry = CSIntFileHeader {
308 name,
309 offset: decryped[0],
310 size: decryped[1],
311 };
312 entries.push(entry);
313 }
314 return Ok(CSIntArc {
315 reader: Arc::new(Mutex::new(reader)),
316 encrypt: Some(encrypt),
317 entries: entries,
318 });
319 }
320 let file_size = reader.seek(SeekFrom::End(0))?;
321 let mut entries = Vec::with_capacity(entry_count as usize);
322 for size in NAME_SIZES {
323 reader.seek(SeekFrom::Start(0x8))?;
324 for _ in 0..entry_count {
325 let name = reader.read_fstring(size, archive_encoding, true)?;
326 if name.is_empty() {
327 entries.clear();
328 break;
329 }
330 let current_offset = reader.stream_position()?;
331 let offset = reader.read_u32()?;
332 let size = reader.read_u32()?;
333 if offset as u64 <= current_offset
334 || !((offset as u64) < file_size
335 && size as u64 <= file_size
336 && offset as u64 <= file_size as u64 - size as u64)
337 {
338 entries.clear();
339 break;
340 }
341 let entry = CSIntFileHeader { name, offset, size };
342 entries.push(entry);
343 }
344 if !entries.is_empty() {
345 return Ok(CSIntArc {
346 reader: Arc::new(Mutex::new(reader)),
347 encrypt: None,
348 entries,
349 });
350 }
351 }
352 Err(anyhow::anyhow!(
353 "Failed to parse archives. Maybe another name length is used? (expected 0x20 or 0x40)",
354 ))
355 }
356
357 fn decrypt_name(name: &mut [u8; 0x40], key: u32, encoding: Encoding) -> Result<String> {
358 let mut k = wrapping! {((key >> 24) + (key >> 16) + (key >> 8) + key) & 0xFF};
359 let mut i = 0;
360 while i < 0x40 && name[i] != 0 {
361 let v = name[i];
362 if v.is_ascii_alphabetic() {
363 let mut j = if v.is_ascii_lowercase() {
364 b'z' - v
365 } else {
366 b'Z' - v + 26
367 } as i8;
368 j -= (k % 0x34) as i8;
369 if j < 0 {
370 j += 0x34;
371 }
372 j = 0x33 - j;
373 name[i] = if j < 26 {
374 b'z' - j as u8
375 } else {
376 b'Z' - (j as u8 - 26)
377 };
378 }
379 k += 1;
380 i += 1;
381 }
382 decode_to_string(encoding, &name[..i], true)
383 }
384
385 fn get_key(password: &str) -> Result<u32> {
386 let bytes = encode_string(Encoding::Cp932, password, true)?;
387 let mut key = 0xFFFFFFFF;
388 for &c in bytes.iter() {
389 key = !CRC32NORMAL_TABLE[((key >> 24) ^ c as u32) as usize] ^ (key << 8);
390 }
391 Ok(key)
392 }
393}
394
395impl<T: Read + Seek + std::fmt::Debug + 'static> Script for CSIntArc<T> {
396 fn default_output_script_type(&self) -> OutputScriptType {
397 OutputScriptType::Json
398 }
399
400 fn default_format_type(&self) -> FormatOptions {
401 FormatOptions::None
402 }
403
404 fn is_archive(&self) -> bool {
405 true
406 }
407
408 fn iter_archive_filename<'a>(
409 &'a self,
410 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
411 Ok(Box::new(self.entries.iter().map(|e| Ok(e.name.clone()))))
412 }
413
414 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
415 Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64))))
416 }
417
418 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
419 if index >= self.entries.len() {
420 return Err(anyhow::anyhow!(
421 "Index out of bounds: {} (max: {})",
422 index,
423 self.entries.len()
424 ));
425 }
426 let entry = &self.entries[index];
427 let mut entry = Entry {
428 header: entry.clone(),
429 reader: self.reader.clone(),
430 pos: 0,
431 script_type: None,
432 };
433 if let Some(encrypt) = &self.encrypt {
434 let mut data = entry.data()?;
435 entry.pos = 0;
436 for i in 0..data.len() / 8 {
437 let j = i * 8;
438 let l = data[j] as u32
439 | (data[j + 1] as u32) << 8
440 | (data[j + 2] as u32) << 16
441 | (data[j + 3] as u32) << 24;
442 let r = data[j + 4] as u32
443 | (data[j + 5] as u32) << 8
444 | (data[j + 6] as u32) << 16
445 | (data[j + 7] as u32) << 24;
446 let result = encrypt.decrypt([l, r]);
447 data[j..j + 4].copy_from_slice(&result[0].to_le_bytes());
448 data[j + 4..j + 8].copy_from_slice(&result[1].to_le_bytes());
449 }
450 return Ok(Box::new(MemEntry {
451 name: entry.header.name.clone(),
452 data: MemReader::new(data),
453 }));
454 }
455 let mut buf = [0u8; 32];
456 let buf_len = match entry.read(&mut buf) {
457 Ok(len) => len,
458 Err(e) => {
459 return Err(anyhow::anyhow!(
460 "Failed to read entry '{}': {}",
461 entry.header.name,
462 e
463 ));
464 }
465 };
466 entry.pos = 0;
467 entry.script_type = detect_script_type(&buf, buf_len, &entry.header.name).copied();
468 Ok(Box::new(entry))
469 }
470}